package chips440.main;

import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Iterator;
import java.awt.image.BufferedImage;
import javax.swing.JOptionPane;
import java.util.HashMap;
import java.io.FileNotFoundException;

import chips440.internals.*;
import chips440.tiles.*;
import chips440.main.Types;

/**
 * A glue layer class to keep the details of implementation away from the IO
 * layer, and the details of the rule semantics and actual playing of the game
 * away from the board.
 *
 * @author Ian Voysey
 * @version 0.1
 */
public class Engine{
    private final int SLEEP = 125;
    private final int THREAD_SLEEP = 10;
    private final int SEC_CONVERT = 1000;
	private final int LOAD_WAIT = 500;
	private final int LOCAL_ID1 = 0;
	private final int LOCAL_ID2 = 1;

    private Types.EngineState game_state;
    private int time_left;
    private int chips_left;
    private int current_level;
    private Board current_map;

    private Thread timer_thread;
    private HashMap<Integer, LinkedList<Types.Direct>> queues;
    private HashMap<Integer, Thread> moves_threads;
    private HashMap<Integer, Player> player_list;

    /**
     * The sole constructor. This only sets up the state; init must be called
     * to start any of the timers relevant to making the game spin.
     */
    public Engine(){
	player_list = new HashMap<Integer, Player>();
	moves_threads = new HashMap<Integer, Thread>();
	queues = new HashMap<Integer, LinkedList<Types.Direct>>();
	game_state = Types.EngineState.ALIVE;
	time_left = 0;
	chips_left = 0;
	current_level = 1;
	current_map = null;
    }

    /**
     * Starts the game playing, creating threads for the player and the engine
     * at large, as well as initializing some state.
     *
     * This method will change signifigantly to mature into a server's
     * connection acception tactic in the next version, where a lot of this
     * overhead will make more difference.
     *
     * @return a unique player identification number
     */
    public Cell init(boolean multiplayer){
	if(timer_thread != null){
	    timer_thread.interrupt();
	    moves_threads.get(LOCAL_ID1).interrupt();
	    moves_threads.get(LOCAL_ID2).interrupt();
	}

	try{
	    current_map = BoardParser.parseLevel(current_level);
	} catch (Exception e){
	    throw new RuntimeException ("first board load failed; can't recover");
	}

	/* these two lines are kind of a hack for non-networked play; in the
	 * eventual server that this engine will become, this till have to be
	 * done more carefully, but since we only have two players, it's not a
	 * big deal.
	 */
	Player player_one =
	    new Player(current_map.getPlayerPositions().get(LOCAL_ID1),
		       0);

	Player player_two =
	    new Player(current_map.getPlayerPositions().get(LOCAL_ID2),
		       1);

	chips_left = current_map.getMaxChip();
	time_left = current_map.getMaxTime();
	player_list.put(LOCAL_ID1, player_one);
	player_list.put(LOCAL_ID2, player_two);

	int id1 = player_one.getID();
	int id2 = player_two.getID();

	queues.put(id1, new LinkedList<Types.Direct>());
	queues.put(id2, new LinkedList<Types.Direct>());

	moves_threads.put(id1, new Thread(new EngineProcessor(id1)));
	moves_threads.put(id2, new Thread(new EngineProcessor(id2)));

	moves_threads.get(id1).start();
	if(multiplayer)
	    moves_threads.get(id2).start();

	timer_thread = new Thread(new Timer());
	timer_thread.start();

	if(multiplayer)
	    return new Cell(id1, id2);
	return new Cell(id1, id1);
    }

    /**
     * fetch the player given an ID
     *
     * @param player_id an integer to identify the player; this method will
     * not function on integers that are not handed out by init
     * @return a cell, assured to contain the player, or null if this is not
     * a valid id
     * @throws IllegalArgumentException if the passed player ID is not known
     */
    public Cell getPlayerPosition (int player_id){
	Player p = player_list.get(player_id);

	if(p == null)
	    throw new IllegalArgumentException("invalid player id number");
	return p.getPosition();
    }

    /**
     * Returns true if the player with the specified ID has the specified item.
     *
     * @param player_id the id of interest. this must be an ID that was handed
     * out by init.
     * @param whichItem the item of interest
     * @throws IllegalArgumentException if the player_id is not a known ID
     * @return true iff the specified player has the specified item
     */
    public boolean hasItem(int player_id, Types.Item whichItem){
	Player p = player_list.get(player_id);
	
	if(p == null)
	    throw new IllegalArgumentException("invalid player id number");
	else 
	    return p.hasItem(whichItem);
    }
	
    /**
     * one player gives an item to another player
     *
     * @param thisItem the item to give
     * @param giver_id the player id of the item giver
     * @param taker_id the player id of the item recipient
     */
    public void giveItem(Types.Item thisItem, int giver_id, int taker_id){
	if(giver_id == taker_id)
		return;
	Player giver = player_list.get(giver_id);
	if(!giver.hasItem(thisItem))
	    return;
	chips440.main.ResourceCache.getSound("ALERT.WAV").play();
	Player taker = player_list.get(taker_id);
	giver.takeItem(thisItem);
	taker.giveItem(thisItem);
	
	// Check for changes to be made to position
	Cell giver_pos = this.getPlayerPosition(giver_id);
	Tile tile = current_map.getTile(giver_pos.getX(), giver_pos.getY());
	Types.TileAct tile_state = tile.okToStep(giver);
	
	switch(tile_state){
		case GOTO:
			queues.get(giver_id).clear();
			move(((MoveTile) tile).getDirection(), giver_id);
			break;
		case KILL:
			game_state = Types.EngineState.DEAD;
	}
    }
    
    /**
     * fetch the remaining time, global to all players
     *
     * @return the number of seconds remaining for the level
     */
    public int getTimeLeft(){
	return time_left;
    }

    /**
     * fetch the number of remaining chips, global to all players
     *
     * @return the number of chips that must be collected before the level is
     * done
     */
    public int getChipsLeft (){
	return chips_left;
    }

    /**
     * fetch the game state
     *
     * @return the game state
     */
    public Types.EngineState getGameState (){
	return game_state;
    }

    /**
     * fetch an image to use
     *
     * @param x the x coordinate to paint at
     * @param y the y coordinate to paint at
     * @return the correct image to paint at pos
     */
    public BufferedImage getImage(int x, int y){
        return this.current_map.getTile(x, y).paintSelf();
    }

    /**
     * Registers a request for a particular player to move in a particular
     * direction. As a security concern, this method has no return type: the
     * player either moves or it doesn't, but it's never told back to the
     * client. The user finds out through changes in the graphical output.
     *
     * @param direction must be one of the four constants defined above
     * @param player_id must be an id handed out by init
     */

    public void move(Types.Direct direction, int player_id){
	LinkedList<Types.Direct> q = queues.get(player_id);
	if(q == null)
	    return;
        q.offer(direction);
    }

    /**
     * move on to the next level if possible, if not assume that the user won!
     *
     * (This is a very optimistic assumption, but in a casual environment, it
     * allows us to make adding and modifying maps profoundly simple.)
     */
    public void nextLevel(){
	current_level++;
	try{
		Thread.sleep(LOAD_WAIT);
	}catch(Exception e){}
	try{
	    current_map = BoardParser.parseLevel(current_level);
	} catch (FileNotFoundException e){
	    JOptionPane.showMessageDialog(null,
					  "You beat the game! (probably)",
					  "Excellent!",
					  JOptionPane.PLAIN_MESSAGE);
	    game_state = Types.EngineState.DEAD;
	    return;
	} catch (IllegalArgumentException e){
	    // no way to fix this, particularly
	}

	chips_left = current_map.getMaxChip();
	time_left = current_map.getMaxTime();

	Player player_one =
	    new Player(current_map.getPlayerPositions().get(LOCAL_ID1),
		       LOCAL_ID1);
	Player player_two =
	    new Player(current_map.getPlayerPositions().get(LOCAL_ID2),
		       LOCAL_ID2);
	player_list.put(LOCAL_ID1, player_one);
	player_list.put(LOCAL_ID2, player_two);

	clearQueues();
	game_state = Types.EngineState.ALIVE;
    }

    /**
     * Time management class (ha ha). This takes care of decrementing the time
     * and loading new levels when appropriate.
     *
     * @author Ryan Hofler
     * @version 0.1
     */
    private class Timer implements Runnable{
	private int sleep_count;

	public Timer(){
	    this.sleep_count = 0;
	}

	public void run(){
	    while(game_state == Types.EngineState.ALIVE){
		if(time_left <= 0)
		    game_state = Types.EngineState.DEAD;
		
		try{
                    Thread.sleep(THREAD_SLEEP);
                }catch(Exception e){}
                sleep_count += THREAD_SLEEP;
		int sleep = sleep_count / SEC_CONVERT;

                if(sleep > 0){
                    time_left -= sleep;
		    sleep_count = 0;
		}

		if(game_state == Types.EngineState.WIN){
		    nextLevel();
		    sleep_count = 0;
		}
	    }
	}	
    }

    /**
     * Thread management class. This encapsulates the tasks needed to be done
     * at a specific time interval (namely processing the move queue), so that
     * they may be neatly passed into a new thread.
     *
     * @author Ryan Hofler
     * @version 0.1
     */
    private class EngineProcessor implements Runnable{
	private int player_id;
	private int sleep_count;
	private LinkedList<Types.Direct> moves_queue;

	/**
	 * The sole constructor, which sets up some small pieces of state.
	 */
        public EngineProcessor(int player_id){
            this.player_id = player_id;
            this.sleep_count = 1;
	    moves_queue = queues.get(player_id);
        }

	/**
	 * The method called by the scheduler when the thread wakes up, which
	 * manages the tasks to be done.
	 */
        public void run(){
            while(game_state == Types.EngineState.ALIVE){
                if(time_left <= 0){
                    continue;
                }
                processMoveQueue();
                try{
                    Thread.sleep(THREAD_SLEEP);
                }catch(Exception e){}

		if(game_state == Types.EngineState.WIN){
		    while(game_state != Types.EngineState.ALIVE)
			;
		}
	    }
        }

        /**
         * goes through the movequeue, attempting to make each move that's
         * been requested since the last pass
         */
        private void processMoveQueue(){
            if(moves_queue.isEmpty())
                return;
	    makeMove(moves_queue.peek(), player_id);
            moves_queue.poll();
        }

	/**
	 * This method actually implements the gorey details of the semantics
	 * of moving a player. If the proposed move contradicts an invariant
	 * (you may not walk through walls, etc.) then it simply doesn't
	 * happen.
	 *
	 * @param direction the move that the player would like to move
	 * @throws IllegalArgumentException iff direction isn't one of this
	 * classes field members or a bad tilestate got into the board
	 */
        public synchronized void makeMove (Types.Direct direction, int player_id){
	    Player player = player_list.get(player_id);
	    if(player == null)
	        return;
	    Cell player_pos = player.getPosition();

	    int x = player_pos.getX();
	    int y = player_pos.getY();
	    switch (direction){
	    case LEFT:
		y--;
		break;
	    case RIGHT:
		y++;
		break;
	    case UP:
		x--;
		break;
	    case DOWN:
		x++;
		break;
	    }

	    Tile nextTile = current_map.getTile(x, y);
	    Types.TileAct tile_state = nextTile.okToStep(player);

	    switch(tile_state){
	    case MOVE:
		break;
	    case OPEN:
		current_map.knockDown(x,y);
		break;
		
	    case ITEM:
		Types.Item item = ((NormalTile)nextTile).getItem();
		if(item == Types.Item.CHIP)
		    chips_left--;
		else
		    player.giveItem(item);
		break;
		
		/* clearing the moves queue stops the player from building up
		 * unwanted moves while on an ice patch or move tiles by idly
		 * poking keys*/
	    case CONT:
		moves_queue.clear();
		player.changePosition(x, y);
		try{ Thread.sleep(SLEEP); }catch(Exception e){}

		sleep_count += SLEEP;
		makeMove(direction, player_id);
		return;

	    case GOTO:
		moves_queue.clear();
		player.changePosition(x, y);
		try{ Thread.sleep(SLEEP);}catch(Exception e){}
		sleep_count += SLEEP;
		makeMove(((MoveTile)nextTile).getDirection(), player_id);
		return;

	    case END:
		if(chips_left == 0)
		    game_state = Types.EngineState.WIN;
		break;
		
	    case KILL:
		game_state = Types.EngineState.DEAD;
		break;

	    case STOP:
		return;
	    }

	    player.changePosition(x, y);
	}
    }

    /**
     * This method empties every player's move queue. This is used when a
     * level is first loaded, to ensure that random keyboard presses during
     * the transition don't effect play.
     */
    private void clearQueues(){
	Iterator it = player_list.keySet().iterator();
	while(it.hasNext()){
	    (queues.get(it.next())).clear();
	}
    }
}
